package org.apache.solr.client.impl;

import java.io.IOException;
import java.io.Reader;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.solr.client.FieldFacet;
import org.apache.solr.client.QueryResults;
import org.apache.solr.client.ResultDoc;
import org.apache.solr.client.SolrClient;
import org.apache.solr.client.exception.SolrClientException;
import org.apache.solr.client.exception.SolrServerException;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

public class ResultsParser
{
	private enum WHAT {
		responseHeader,
		result,
		debug,
		highlighting,
		facet_counts;
		
		public static WHAT get( String v )
		{
			if( v != null ) {
				try {
					return WHAT.valueOf( v );
				}
				catch( Exception ex ) {}
			}
			return null;
		}
	}
	
	private enum TYPE 
	{
		LST   { public Object  parse( String v ) { return null; } },
		ARR   { public Object  parse( String v ) { return null; } },
		INT   { public Integer parse( String v ) { return Integer.valueOf( v ); } },
		LONG  { public Long    parse( String v ) { return Long.valueOf( v ); } },
		BOOL  { public Boolean parse( String v ) { return Boolean.valueOf( v ); } },
		FLOAT { public Float   parse( String v ) { return Float.valueOf( v );   } },
		DATE  { public Date    parse( String v ) throws ParseException { return SolrClient.ISO8601_UTC.parse( v.replace('.',',') ); } },
		STR   { public String  parse( String v ) { return v;    } };
		
		public static TYPE get( String v )
		{
			if( v != null ) {
				try {
					return TYPE.valueOf( v.toUpperCase() );
				}
				catch( Exception ex ) {}
			}
			return null;
		}
		
		public abstract Object parse( String v ) throws ParseException;
	}
	
	
	
	/**
	 * Factory for parsers for the XML we get back from the server.
	 */
	protected XmlPullParserFactory factory;

	public ResultsParser( XmlPullParserFactory f ) throws XmlPullParserException
	{
		factory = f;
	}
	
    public QueryResults process( Reader reader ) throws SolrClientException, SolrServerException, XmlPullParserException, IOException
    {
    	QueryResults res = new QueryResults();

		try {
			XmlPullParser xpp = null;
			try {
				xpp = factory.newPullParser();
				xpp.setInput(reader);
				xpp.nextTag();
			} 
			catch (XmlPullParserException e) {
				throw new SolrClientException( "XML parsing exception in solr client", e);
			}

			if (!"response".equals(xpp.getName())) {
				throw new SolrClientException( "Response from server is not rooted with a <response> tag. ["+xpp.getName()+"]" );
			}
			
			int eventType = xpp.next();
	        while( true ) {
	        	if( eventType == XmlPullParser.END_DOCUMENT ) {
	                return res; // finish function...
	        	}
	        	if( eventType == XmlPullParser.START_TAG ) {
	        		WHAT what = WHAT.get( xpp.getName() );
	        		if( what == null ) {
	        			String n = xpp.getAttributeValue( null, "name" );
	        			what = WHAT.get( n );
	        			if( what == null ) {
	        				throw new SolrClientException( "unknown WHAT: "+xpp.getName() + " / " + n );
		        		}
	        		}
	        		
	        		 switch( what )
	        		 {
	        		 case responseHeader:  processResponseHeader( xpp, res ); break;
	        		 case result:          processResults( xpp, res ); break;
	        		 case highlighting:    processHighlighting( xpp, res ); break;
	        		 case debug:           processDebugInfo( xpp, res ); break;
	        		 case facet_counts:    processFacetInfo(xpp, res); break;
	        		 default:
	        			 throw new SolrClientException( "unhandled what: "+what  );
	        		 }
	        	}
	        	else {
	        		//System.out.println( "unhandled:"+eventType );
	        	}
	            eventType = xpp.next();
	        } 
		} 
		catch( ParseException ex ) {
			 throw new SolrClientException( "parse exception", ex  );
		}
		finally {
			reader.close();
		}
		//return res;
    }
    
    private Map<String,Object> parseFieldMap( XmlPullParser xpp ) throws SolrClientException, XmlPullParserException, IOException, ParseException
    {
    	Map<String,Object> map = new HashMap<String, Object>();
    	
    	int event = xpp.nextTag();
    	while( event == XmlPullParser.START_TAG ) {
    		TYPE type = TYPE.get( xpp.getName() );
    		if( type == null ) {
                throw new SolrClientException( "unknown type: "+xpp.getName() );
    		}
    		String name = xpp.getAttributeValue( null, "name" );
    		switch( type )
    		{
    		case LST:
    			 // this actually looses the order, but oh well!
    			 // for now, it just looks like it is used for debug..
   			     map.put( name, parseFieldMap( xpp ) );
   			     break;
                 
    		case ARR: {
	    		event = xpp.nextTag();
    			TYPE current = null;
    			List<Object> arr = new ArrayList<Object>( 5 );
    			while( event == XmlPullParser.START_TAG ) {
    	    		TYPE t2 = TYPE.get( xpp.getName() );
    	    		if( t2 == null ) {
    	                throw new SolrClientException( "unknown type: "+type );
    	    		}
    	    		if( current == null ) {
    	    			current = t2;
    	    		}
    	    		else if (current != t2 ) {
    	                throw new SolrClientException( "can not switch type on an array! "+t2 +"!="+current );
    	    		}
    	    		
    	    		switch( t2 )
    	    		{
    	    		case LST:
    	    		case ARR: 
   	                     throw new SolrClientException( t2 + " can not be nested!" );

    	    		case INT:
    	    		case FLOAT:
    	    		case DATE:
    	    		case STR:
    	    			 arr.add( t2.parse( xpp.nextText().trim() ) );
    				     break;
    	    			
    	    		default:
    	                throw new SolrClientException( "unhandled type: "+t2 );
    	    		}
    	    		event = xpp.nextTag();
    	    	}
  			    map.put( name, arr );
    		} break;

    		case INT:
    		case LONG:
    		case FLOAT:
    		case DATE:
    		case BOOL:
    		case STR:
   			     map.put( name, type.parse( xpp.nextText().trim() ) );
			     break;
    			
    		default:
                throw new SolrClientException( "unhandled type: "+type );
    		}
    		event = xpp.nextTag();
    	}
    	return map;
    }
    

    @SuppressWarnings("unchecked")
	private void processResponseHeader( XmlPullParser xpp, QueryResults res) throws XmlPullParserException, IOException, SolrClientException, ParseException 
    {
        Map<String,Object> map = parseFieldMap( xpp );
        res.setQTime(  (Integer)map.get("QTime") );
        res.setStatus( (Integer)map.get("status") );
        
        Map<String,Object> params = (Map<String, Object>) map.get( "params" );
        if( params != null ) {
        	Map<String,String> pmap = new HashMap<String, String>();
        	for( Map.Entry<String, Object> p : params.entrySet()) {
        		Object o = p.getValue();
        		if( o instanceof String ) {
        			pmap.put( p.getKey(), (String)o );
        		}
        		else {
        			// not sure what to do, this is typically the facet thing...
        		}
        	}
        	res.setParams( pmap );
        }
	}

    private void processResults( XmlPullParser xpp, QueryResults res) throws XmlPullParserException, IOException, SolrClientException, ParseException 
    {
    	String v = xpp.getAttributeValue( null, "numFound" );
    	if( v != null ) 
    		res.setNumFound( Integer.parseInt( v ) );
    	
    	v = xpp.getAttributeValue( null, "start"    );
    	if( v != null ) 
    		res.setStart( Integer.parseInt( v ) );
    	
    	v = xpp.getAttributeValue( null, "maxScore" );
    	if( v != null ) 
    		res.setMaxScore( Float.parseFloat( v ) );
    	
    	int event = xpp.nextTag();
    	while( event == XmlPullParser.START_TAG ) {
    		if( !"doc".equals( xpp.getName() ) ) {
                throw new SolrClientException( "should be doc, not: "+xpp.getName() );
    		}
    		Map<String,Object> map = parseFieldMap( xpp );
    		String id = (String)map.remove( "id" );
    		Float score = (Float)map.remove( "score" );
    		ResultDoc doc = new ResultDoc( id, score );
    		doc.setField( map );
    		res.addDoc( doc );
    		event = xpp.nextTag();
    	}
	}

    @SuppressWarnings("unchecked")
	private void processHighlighting( XmlPullParser xpp, QueryResults res) throws XmlPullParserException, IOException, SolrClientException , ParseException
    {
    	int event = xpp.nextTag();
    	while( event == XmlPullParser.START_TAG ) {
    		if( !"lst".equals( xpp.getName() ) ) {
                throw new SolrClientException( "should be lst, not: "+xpp.getName() );
    		}
    		String id = xpp.getAttributeValue( null, "name" );
    		Map<String,Object> map = parseFieldMap( xpp );
    		
    		ResultDoc doc = res.getDoc( id );
    		if( doc != null ) {
    			// transform this to a List<String>
				for( Map.Entry<String, Object> field : map.entrySet()) {
					String name = field.getKey();
					List lst = (List)field.getValue();
    				String[] arr = (String[])lst.toArray( new String[lst.size()] );
    				doc.setHighlight( name, arr );
    			}
    		}
    		else {
        		System.out.println( id + " [HIGHLIGHT] "+map );
    		}
    		event = xpp.nextTag();
    	}
	}

    @SuppressWarnings("unchecked")
	private void processDebugInfo( XmlPullParser xpp, QueryResults res) throws XmlPullParserException, IOException, SolrClientException, ParseException 
    {
        Map<String,Object> map = parseFieldMap( xpp );
        
        QueryResults.DebugInfo info = new QueryResults.DebugInfo();
        info.setRawQueryString( (String)map.remove( "rawquerystring") );
        info.setQueryString(    (String)map.remove( "querystring") );
        info.setParsedQuery(    (String)map.remove( "parsedquery") );
        info.setParsedQueryToString( (String)map.remove( "parsedquery_toString") );
        res.setDebugInfo( info );
        
        Map<String,Object> exp = (Map<String,Object>)map.remove( "explain" );
		for( Map.Entry<String, Object> field : exp.entrySet()) {
			String name = field.getKey();
			
        	// name: id=F5D5WOUEVVNWNN7,internal_docid=4
        	StringTokenizer st = new StringTokenizer( name, "=, " );
        	st.nextToken(); // skip "id"
        	String id = st.nextToken();
        	st.nextElement(); // skip internal_docid
        	String docID = st.nextToken();
        	
        	ResultDoc doc = res.getDoc( id );
        	if( doc == null ) {
                throw new SolrClientException( "can not find doc with ID: "+id );
        	}
        	doc.setInternalDocID( Integer.valueOf( docID ) );
        	doc.setExplain( (String)field.getValue() );
        }
        
        if( !map.keySet().isEmpty() ) {
            System.out.println( "UNUSED DEBUG INFO:"+map );
        }
	}
    
    

	private void processFacetInfo( XmlPullParser xpp, QueryResults res) throws XmlPullParserException, IOException, SolrClientException, ParseException 
    {
    	int event = xpp.nextTag();
    	while( event == XmlPullParser.START_TAG ) {
    		if( !"lst".equals( xpp.getName() ) ) {
                throw new SolrClientException( "should be lst, not: "+xpp.getName() );
    		}
    		
    		String name = xpp.getAttributeValue( null, "name" );
    		
    		if( "facet_fields".equals( name ) ) {
    			event = xpp.nextTag();
    	    	while( event == XmlPullParser.START_TAG ) {
    	    		if( !"lst".equals( xpp.getName() ) ) {
    	                throw new SolrClientException( "should be lst, not: "+xpp.getName() );
    	    		}
    	    		FieldFacet facet = new FieldFacet( xpp.getAttributeValue( null, "name" ) );
    	    		
    	    		// now loop through each of the children...
    	    		event = xpp.nextTag();
        	    	while( event == XmlPullParser.START_TAG ) {
        	    		if( !"int".equals( xpp.getName() ) ) {
        	                throw new SolrClientException( "should be int, not: "+xpp.getName() );
        	    		}
        	    		facet.add( 
        	    		   xpp.getAttributeValue( null, "name" ),
        	    		   Integer.parseInt( xpp.nextText() )
       	    		    );
        	    		
        	     		event = xpp.nextTag();
        	    	}
        	    	res.addFieldFacet( facet );
    	     		event = xpp.nextTag();
    	    	}
    		}
    		else {
    			//System.out.println( "unused: "+name );
    			parseFieldMap( xpp ); // have to do this to eat it all up

        		// Map<String,Object> map = parseFieldMap( xpp );
         		//System.out.println( " >>"+map );
    		}
    		
     		event = xpp.nextTag();
    	}
	}
}